#include "material.h"
#include "util.h"
#include "hittable.h"
#include "texture.h"
#include "pdf.h"

bool Material::scatter(const Ray& r_in, const HitRecord& rec, Color& attenuation, Ray& scattered, double& pdf)const
{
	return false;
}

bool Material::scatter(const Ray& r_in, const HitRecord& rec, ScatterRecord& srec) const
{
	return false;
}

Color Material::emitted(const Ray& r_in, const HitRecord& rec, double u, double v, const Point3& p) const
{
	return Color(0, 0, 0);
}

double Material::scattering_pdf(const Ray& r_in, const HitRecord& rec, const Ray& scattered)const
{
	return 0.0;
}

Lambertian::Lambertian(const Color& albedo) :tex(make_shared<SolidColor>(albedo))
{
}

Lambertian::Lambertian(std::shared_ptr<Texture> albedo) : tex(albedo)
{
}

bool Lambertian::scatter(const Ray& r_in, const HitRecord& rec, Color& attenuation, 
	Ray& scattered, double& pdf) const
{

	Onb uvw;
	uvw.build_from_w(rec.normal);
	auto scatter_direction = uvw.local(random_cosine_direction());

	scattered = Ray(rec.p, unit_vector(scatter_direction), r_in.time());
	attenuation = tex->value(rec.u, rec.v, rec.p);
	pdf = dot(uvw.w(), scattered.direction()) / pi;

	//auto scatter_direction = rec.normal + random_unit_vector();
	//// Catch degenerate scatter direction
	//if (scatter_direction.near_zero())
	//    scatter_direction = rec.normal;

	//scattered = Ray(rec.p, scatter_direction,r_in.time());
	//attenuation = tex->value(rec.u,rec.v,rec.p);

	return true;
}

bool Lambertian::scatter(const Ray& r_in, const HitRecord& rec, ScatterRecord& srec) const
{
	srec.attenuation = tex->value(rec.u, rec.v, rec.p);
	srec.pdf_ptr = make_shared<CosinePdf>(rec.normal);
	srec.skip_pdf = false;
	return true;
}

double Lambertian::scattering_pdf(const Ray& r_in, const HitRecord& rec, const Ray& scattered) const
{
	auto cos_theta = dot(rec.normal, unit_vector(scattered.direction()));
	return cos_theta < 0 ? 0 : cos_theta / pi;
}

Metal::Metal(const Color& albedo, double fuzz) :albedo(albedo), fuzz(fuzz)
{
}

bool Metal::scatter(const Ray& r_in, const HitRecord& rec, Color& attenuation, Ray& scattered, double& pdf) const
{
	Vec3 reflected = reflect(r_in.direction(), rec.normal);
	reflected = unit_vector(reflected) + (fuzz * random_unit_vector());
	scattered = Ray(rec.p, reflected, r_in.time());
	attenuation = albedo;
	return (dot(scattered.direction(), rec.normal) > 0);
}

bool Metal::scatter(const Ray& r_in, const HitRecord& rec, ScatterRecord& srec) const
{
	Vec3 reflected = reflect(r_in.direction(), rec.normal);
	reflected = unit_vector(reflected) + (fuzz * random_unit_vector());

	srec.attenuation = albedo;
	srec.pdf_ptr = nullptr;
	srec.skip_pdf = true;
	srec.skip_pdf_ray = Ray(rec.p, reflected, r_in.time());

	return true;
}

Dielectric::Dielectric(double refraction_index) : refraction_index(refraction_index) {}

bool Dielectric::scatter(const Ray& r_in, const HitRecord& rec, Color& attenuation, Ray& scattered, double& pdf) const
{
	attenuation = Color(1.0, 1.0, 1.0);
	double ri = rec.front_face ? (1.0 / refraction_index) : refraction_index;

	Vec3 unit_direction = unit_vector(r_in.direction());

	double cos_theta = fmin(dot(-unit_direction, rec.normal), 1.0);
	double sin_theta = sqrt(1.0 - cos_theta * cos_theta);

	bool cannot_refract = ri * sin_theta > 1.0;
	Vec3 direction;

	if (cannot_refract || reflectance(cos_theta, ri) > random_double())
		direction = reflect(unit_direction, rec.normal);
	else
		direction = refract(unit_direction, rec.normal, ri);

	scattered = Ray(rec.p, direction, r_in.time());
	return true;
}

bool Dielectric::scatter(const Ray& r_in, const HitRecord& rec, ScatterRecord& srec) const
{
	srec.attenuation = Color(1.0, 1.0, 1.0);
	srec.pdf_ptr = nullptr;
	srec.skip_pdf = true;

	double ri = rec.front_face ? (1.0 / refraction_index) : refraction_index;

	Vec3 unit_direction = unit_vector(r_in.direction());

	double cos_theta = fmin(dot(-unit_direction, rec.normal), 1.0);
	double sin_theta = sqrt(1.0 - cos_theta * cos_theta);

	bool cannot_refract = ri * sin_theta > 1.0;
	Vec3 direction;

	if (cannot_refract || reflectance(cos_theta, ri) > random_double())
		direction = reflect(unit_direction, rec.normal);
	else
		direction = refract(unit_direction, rec.normal, ri);

	srec.skip_pdf_ray = Ray(rec.p, direction, r_in.time());

	return true;
}

double Dielectric::reflectance(double cosine, double refraction_index)
{
	// Use Schlick's approximation for reflectance.
	auto r0 = (1 - refraction_index) / (1 + refraction_index);
	r0 = r0 * r0;
	return r0 + (1 - r0) * pow((1 - cosine), 5);
}

DiffuseLight::DiffuseLight(std::shared_ptr<Texture> tex) :tex(tex)
{
}

DiffuseLight::DiffuseLight(const Color& emit) : tex(make_shared<SolidColor>(emit))
{
}

Color DiffuseLight::emitted(const Ray& r_in, const HitRecord& rec, double u, double v, const Point3& p) const
{
	if (!rec.front_face)
		return Color(0, 0, 0);
	return tex->value(u, v, p);
}

Isotropic::Isotropic(const Color& albedo)
	: tex(make_shared<SolidColor>(albedo)) {}

Isotropic::Isotropic(shared_ptr<Texture> tex)
	: tex(tex) {}

bool Isotropic::scatter(const Ray& r_in, const HitRecord& rec, Color& attenuation,
	Ray& scattered, double& pdf) const
{
	scattered = Ray(rec.p, random_unit_vector(), r_in.time());
	attenuation = tex->value(rec.u, rec.v, rec.p);
	pdf = 1 / (4 * pi);

	return true;
}

bool Isotropic::scatter(const Ray& r_in, const HitRecord& rec, ScatterRecord& srec) const
{
	srec.attenuation = tex->value(rec.u, rec.v, rec.p);
	srec.pdf_ptr = make_shared<SpherePdf>();
	srec.skip_pdf = false;

	return true;
}

double Isotropic::scattering_pdf(const Ray& r_in, const HitRecord& rec, const Ray& scattered) const
{
	return 1 / (4 * pi);
}

